Skip to content

Wasm splitting in yew#3932

Open
WorldSEnder wants to merge 6 commits intoyewstack:masterfrom
WorldSEnder:split-wasm
Open

Wasm splitting in yew#3932
WorldSEnder wants to merge 6 commits intoyewstack:masterfrom
WorldSEnder:split-wasm

Conversation

@WorldSEnder
Copy link
Member

Description

Add a way to split the wasm bundle in multiple components. This modifies the build process, and uses relocation information emitted by llvm to identify where to "split". There's a bit of glue code in yew to ensure that messages sent to the lazy component are processed and properties are passed along without additional cloning.

The main part of the solution lives in https://github.com/WorldSEnder/wasm-split-prototype as of now. This was implemented in collaboration with the maintainer of leptos.

Checklist

  • I have reviewed my own code
  • I have added examples

@WorldSEnder WorldSEnder requested review from jstarry and ranile October 14, 2025 21:01
github-actions[bot]
github-actions bot previously approved these changes Oct 14, 2025
@WorldSEnder
Copy link
Member Author

WorldSEnder commented Oct 14, 2025

The review pings are mostly because I think both of you will find this interesting, not necessarily as an invite to dig into the code and give meaningful suggestions for improvements (but feel free to if you have the time).

github-actions[bot]
github-actions bot previously approved these changes Oct 16, 2025
@github-actions
Copy link

github-actions bot commented Oct 16, 2025

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.126 ns      │ 2.718 ns      │ 2.129 ns      │ 2.143 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.175 ns      │ 2.448 ns      │ 2.177 ns      │ 2.186 ns      │ 100     │ 1000000000

@github-actions
Copy link

github-actions bot commented Oct 16, 2025

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.782 313.926 311.242 0.949
Hello World 10 472.864 517.409 480.205 13.447
Function Router 10 33734.047 35029.482 34254.971 400.728
Concurrent Task 10 1005.973 1007.625 1006.609 0.558
Many Providers 10 1082.568 1153.712 1101.512 20.348

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 310.786 311.436 311.053 0.231
Hello World 10 488.157 510.171 497.526 7.603
Function Router 10 32721.830 33785.998 33228.638 335.329
Concurrent Task 10 1005.237 1008.239 1007.174 0.921
Many Providers 10 1091.467 1125.493 1110.913 12.555

github-actions[bot]
github-actions bot previously approved these changes Oct 22, 2025
@github-actions
Copy link

github-actions bot commented Oct 22, 2025

Visit the preview URL for this PR (updated for commit 3a930f1):

https://yew-rs-api--pr3932-split-wasm-ahe0tdp5.web.app

(expires Wed, 18 Mar 2026 19:15:54 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

let suspension = Suspension::from_future(async move {
// Ignore error in case receiver was dropped
let vtable = C::fetch().await;
let comp = (vtable.imp.create)(&creation_ctx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates the underlying component with creation_ctx, whose link() returns inner_scope. But inner_scope is never mounted — its state is permanently None.

This makes function component hooks permanently broken after load.

When a function component is created, FunctionComponent::new() captures ctx.link().clone() into a re_render closure:

  let re_render = {
      let link = ctx.link().clone(); // this is inner_scope
      Rc::new(move || link.send_message(()))
  };

Every hook (use_state, use_reducer, etc.) shares this closure. When a state setter fires, it calls inner_scope.send_message(()), which schedules an UpdateRunner. But UpdateRunner::run() checks inner_scope.state, finds None, and silently returns. The component renders once correctly and then becomes completely unresponsive to all state changes. I've confirmed this — a simple use_state counter wrapped in declare_lazy_component! renders 0 and clicking the increment button does nothing.

I created a minimal reproducing repo https://github.com/Madoshakalaka/yew-lazy-hook-bug

Copy link
Member Author

@WorldSEnder WorldSEnder Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the same is true for all messages sent to the components own link which do not get drained correctly. Though both pending_messages and state are Rc, downcasting will later later lead to failures due to misatching generics Lazy<C>!= C and LazyMessage<C::Message> != C::Message.

don't start fetching once per component, cache the vtable.
makes it possible to implement a helper macro without the user
pulling in an extra dependency and has tighter version requirements.
@github-actions
Copy link

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 101.058 101.115 +0.058 +0.057%
boids 168.884 168.948 +0.064 +0.038%
communication_child_to_parent 94.509 94.574 +0.065 +0.069%
communication_grandchild_with_grandparent 106.349 106.409 +0.061 +0.057%
communication_grandparent_to_grandchild 102.694 102.759 +0.064 +0.063%
communication_parent_to_child 91.922 91.979 +0.057 +0.062%
contexts 106.418 106.469 +0.051 +0.048%
counter 87.232 87.298 +0.065 +0.075%
counter_functional 89.270 89.327 +0.058 +0.065%
dyn_create_destroy_apps 91.148 91.204 +0.056 +0.061%
file_upload 100.327 100.383 +0.056 +0.055%
function_delayed_input 95.241 95.306 +0.064 +0.068%
function_memory_game 174.099 174.157 +0.059 +0.034%
function_router 396.017 396.077 +0.061 +0.015%
function_todomvc 165.386 165.451 +0.065 +0.040%
futures 235.988 236.046 +0.058 +0.024%
game_of_life 105.533 105.599 +0.065 +0.062%
immutable 260.578 260.643 +0.064 +0.025%
inner_html 81.775 81.833 +0.058 +0.070%
js_callback 110.395 110.453 +0.059 +0.053%
keyed_list 180.710 180.771 +0.062 +0.034%
mount_point 85.148 85.204 +0.056 +0.065%
nested_list 114.093 114.154 +0.062 +0.054%
node_refs 92.521 92.577 +0.057 +0.061%
password_strength 1719.693 1719.756 +0.062 +0.004%
portals 93.995 94.048 +0.053 +0.056%
router 366.676 366.722 +0.046 +0.013%
split-wasm N/A 254.434 N/A N/A
suspense 114.399 114.460 +0.061 +0.053%
timer 89.370 89.436 +0.065 +0.073%
timer_functional 99.805 99.865 +0.061 +0.061%
todomvc 143.098 143.155 +0.058 +0.040%
two_apps 87.146 87.211 +0.065 +0.075%
web_worker_fib 137.046 137.160 +0.114 +0.083%
web_worker_prime 188.232 188.354 +0.121 +0.064%
webgl 83.922 83.985 +0.063 +0.076%

✅ None of the examples has changed their size significantly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants